#include <boost/program_options.hpp>
namespace po = boost::program_options;
#include <boost/asio.hpp>
using boost::asio::ip::tcp;
#include <boost/filesystem.hpp>
#include <boost/array.hpp>
#include <boost/algorithm/string/predicate.hpp>
#include <iostream>
#include <iterator>
#include <algorithm>
#include <string>
#include <fstream>
#include "UriCodec.h"


std::string expand_user(std::string path) {
  if (!path.empty() && path[0] == '~') {
    char const* home = getenv("HOME");
    if (home || ((home = getenv("USERPROFILE")))) {
      path.replace(0, 1, home);
    }
    else {
      char const *hdrive = getenv("HOMEDRIVE"),
        *hpath = getenv("HOMEPATH");
      assert(hdrive);  // or other error handling
      assert(hpath);
      path.replace(0, 1, std::string(hdrive) + hpath);
    }
  }
  return path;
}

int find_index_of_commandlet_id_in_arguments(int argc, char** argv) {
	bool option_value_expected = false;
	for (int i = 1; i < argc; i++) {
		char* arg = argv[i];
		if (option_value_expected) {
			// The next argument is not expected to be an option value because this is already the option value
			option_value_expected = false;
		} else {
			bool is_option_key = boost::starts_with(arg, "--");
			if (is_option_key) {
				// The next argument is expected to be an option value. except for flag options. help is a flag option.
				option_value_expected = std::string("--help").compare(arg) != 0;
			} else {
				// This is supposed to be the first positional option - our commandlet id
				return i;
			}
		}
	}
	return -1;
}

int main(int argc, char** argv)
{
	try {
		// Configure
	
		std::string host;
		std::string port;
		std::string context_path;
		std::vector<std::string> commandlet_args;

		// Declare the supported options.
		po::options_description desc("Allowed options");
		desc.add_options()
			("help", "produce help message")
			("host", po::value<std::string>(&host)->default_value("localhost"), "commandlet server host")
			("port", po::value<std::string>(&port)->default_value("8181"), "commandlet server port")
			("context_path", po::value<std::string>(&context_path)->default_value("/commandlet/execute"), "commandlet server context path")
		;

		int commandlet_id_index = find_index_of_commandlet_id_in_arguments(argc, argv);

		if (commandlet_id_index < 0) {
			std::cerr << desc << "\n";
			return 1;
		}

		po::variables_map vm;
		// Let the boost program option parser only parse arguments left to the commandlet id
		po::parsed_options parsed = po::command_line_parser(commandlet_id_index, argv).options(desc).run();
		po::store(parsed, vm);
		po::notify(vm);

		if (vm.count("help")) {
			std::cout << desc << "\n";
			return 0;
		}


		
		// Read config file
		std::string path_to_config_file = expand_user("~/.commandlet-platform/config.properties");


		std::ifstream ifs(path_to_config_file.c_str());
	
		if (ifs)
		{
			store(parse_config_file(ifs, desc), vm);
			notify(vm);
		}

		std::string commandlet_id (argv[commandlet_id_index]);

		// Join commandlet arguments
		std::string arg_string = "";
		bool first = true;
		for (int i = commandlet_id_index + 1; i < argc; i++) {
			std::string arg(argv[i]);
			if (first) first = false; else arg_string += "&";
			arg_string += "arg=" + UriEncode(arg);
		}

		// Determine working directory
		boost::filesystem::path wd_path(boost::filesystem::current_path());
			

		// Connect

		boost::asio::io_service io_service;
		tcp::resolver resolver(io_service);
		tcp::resolver::query query(host, port);
		tcp::resolver::iterator endpoint_iterator = resolver.resolve(query);
		tcp::resolver::iterator end;
		tcp::socket socket(io_service);
		boost::system::error_code error = boost::asio::error::host_not_found;
		while (error && endpoint_iterator != end)
		{
			socket.close();
			socket.connect(*endpoint_iterator++, error);
		}
		if (error)
			throw boost::system::system_error(error);

		// Build the request
		boost::asio::streambuf request;
		std::ostream request_stream(&request);
		std::string encoded_commandlet_id = UriEncode(commandlet_id);
		std::string encoded_wd = UriEncode(wd_path.string());
		request_stream << "GET " + context_path + "?id=" + encoded_commandlet_id + "&wd=" + encoded_wd + "&" + arg_string + " HTTP/1.0\r\n";
		request_stream << "Host: " << host << "\r\n";
		request_stream << "Accept: */*\r\n";
		request_stream << "Connection: close\r\n\r\n";

		// Send the request
		boost::asio::write(socket, request);

		// Read the response status line
		boost::asio::streambuf response;
		boost::asio::read_until(socket, response, "\r\n");

		// Check that the response is OK
		std::istream response_stream(&response);
		std::string http_version;
		response_stream >> http_version;
		unsigned int status_code;
		response_stream >> status_code;
		std::string status_message;
		std::getline(response_stream, status_message);
		if (!response_stream || http_version.substr(0, 5) != "HTTP/")
		{
			std::cerr << "Invalid response\n";
			return 1;
		}
		if (status_code != 200)
		{
			std::cerr << "Response returned with status code " << status_code << "\n";
			return 1;
		}

		// Read and process the response headers, which are terminated by a blank line.
		boost::asio::read_until(socket, response, "\r\n\r\n");
		std::string header;
		while (std::getline(response_stream, header) && header != "\r") {}
			

		// Write whatever content we already have to output
		if (response.size() > 0)
			std::cout << &response;

		// Read until EOF, writing data to output as we go.
			
		boost::asio::streambuf b;
		for (;;)
		{
			boost::system::error_code error;
				
			//boost::array<char, 128> buf;
			//size_t len = socket.read_some(boost::asio::buffer(buf), error);
			size_t len = boost::asio::read_until(socket, b, "\n", error);
			if (error == boost::asio::error::eof)
				break; // Connection closed cleanly by peer.
			else if (error)
				throw boost::system::system_error(error); // Some other error.
				
			//cout.write(buf.data(), len);
			// b.commit(len);
			std::istream is(&b);
			std::string line;
			std::getline(is, line);
			//char * line = new char[len];
			//is.get(line, len);
			std::cout << line << "\n";
			std::cout.flush();
		}
	}
	catch (std::exception& e)
	{
		std::cerr << e.what() << std::endl;
		return 1;
	}

	return 0;
}
